from __future__ import annotations

import math
from dataclasses import dataclass
from typing import Tuple

from common.typing_utils import numeric


@dataclass
class BoundingBox(object):
    """Bounding Box in geographical or projected coordinate system.

    Parameters
    ----------
    left: numeric
        x-coordinate of the left of the bounding box
    bottom: numeric
        y-coordinate of the bottom of the bounding box
    right: numeric
        x-coordinate of the right of the bounding box
    upper: numeric
        y-coordinate of the upper of the bounding box
    """

    left: numeric
    bottom: numeric
    right: numeric
    upper: numeric

    @staticmethod
    def from_shapely_bounds(
        shapely_bounds: Tuple[numeric, numeric, numeric, numeric]
    ) -> BoundingBox:
        """convert from shapely's bounds, with coords in the order of (left, bottom, right, upper)"""
        minx, miny, maxx, maxy = shapely_bounds
        return BoundingBox(left=minx, right=maxx, upper=maxy, bottom=miny)

    @property
    def xsize(self) -> numeric:
        return abs(self.right - self.left)

    @property
    def ysize(self) -> numeric:
        return abs(self.upper - self.bottom)

    def to_shapely_bounds(self) -> Tuple[numeric, numeric, numeric, numeric]:
        """return bounds in shapely order, (left, bottom, right, upper)"""
        return (self.left, self.bottom, self.right, self.upper)

    def to_gdal_target_extent(self) -> Tuple[numeric, numeric, numeric, numeric]:
        """return bounds in gdal's target extent order, (left, bottom, right, upper)"""
        return (self.left, self.bottom, self.right, self.upper)

    def to_gdal_projected_window(self) -> Tuple[numeric, numeric, numeric, numeric]:
        """return bounds in gdal's projected window order, (left, upper, right, bottom)"""
        return (self.left, self.upper, self.right, self.bottom)

    def nudge(self, resolution: numeric) -> BoundingBox:
        """Nudge the bounding box to have all coordinates as multiples of a given resolution.
        Just a short cut to the `nudge_bbox` function.
        """
        return nudge_bbox(self, resolution)


def nudge_bbox(bbox: BoundingBox, resolution: numeric) -> BoundingBox:
    """Nudge a given bounding box to have all coordinates as multiples of a given resolution."""
    left = math.floor(bbox.left / resolution) * resolution
    right = math.ceil(bbox.right / resolution) * resolution
    bottom = math.floor(bbox.bottom / resolution) * resolution
    upper = math.ceil(bbox.upper / resolution) * resolution
    return BoundingBox(left=left, right=right, upper=upper, bottom=bottom)
